Published on

Django 매월 특정 날짜만(마지막 날짜) 가져오기

Authors

매월(month) 마지막 날짜의 데이터를 연도(year)만 입력했을 때 필터링하여 가져오는 방법입니다.

특정 페이지에서 연도별 차트를 가져올 때, 매월 마지막 날짜만 가져와야 합니다.

예를들어서 6월 30일에 API 조회를 한다면 1월 31일/2월 29일(혹은 28일)/3월31일/…/6월 30일 의 데이터만 가져오는 것 입니다. (일별 데이터는 매일 백그라운드(celery&aws event bridge&cron)로 생성하고 있기 때문에 마지막 일 여부에 대해서는 보장이 되어있습니다.)

떠오른 방법은 2가지이고 2번째 방법에 대해서 다뤄보겠습니다.

  1. 특정 일을 따로 저장하거나 세팅해두고 (ex: [1월 31일,…]) datefield 기준 __in 을 사용해서 가져오는 방법도 있습니다.
  2. 월로 그룹화하고 max() 매소드로, 매월 가장 마지막 날짜를 가져오는 방법 입니다.

코드

import django_filters
from django.db.models import Max
from django.utils.timezone import make_aware
from datetime import datetime, timedelta
from django.db.models.functions import ExtractMonth
from .models import InvestmentResult

class InvestmentResultFilter(django_filters.FilterSet):
		#  이번 시간에 사용할 필터링
    year = django_filters.NumberFilter(method='filter_by_year')

    #  month 는 무시
    month = django_filters.NumberFilter(field_name="invested_at", lookup_expr="month")

    class Meta:
        model = InvestmentResult
        fields = ['year', 'month']

    def filter_by_year(self, queryset, name, value):
        # 연도별로 필터링
        start_date = make_aware(datetime(value, 1, 1))
        end_date = make_aware(datetime(value + 1, 1, 1)) - timedelta(seconds=1)
        queryset = queryset.filter(invested_at__range=(start_date, end_date))

        # 각 월의 마지막 날짜 가져오기
        last_days = (
            queryset
            .annotate(month=ExtractMonth('invested_at'))
            .values('month')
            .annotate(last_day=Max('invested_at'))
        )

        # 마지막 날짜의 데이터만 필터링
        last_day_dates = [entry['last_day'] for entry in last_days]
        return queryset.filter(invested_at__in=last_day_dates)

Filters

  1. django_filters 사용하여 필터셋 클래스를 정의합니다.
  2. year 필터는 커스텀 메서드를 사용하고, month 필터는 기본 필터링 기능을 사용합니다.

ORM

1. 기간 세팅


start_date = make_aware(datetime(value, 1, 1))
end_date = make_aware(datetime(value + 1, 1, 1)) - timedelta(seconds=1)
queryset = queryset.filter(invested_at__range=(start_date, end_date))

  • start_date는 주어진 연도의 1월 1일을 나타냅니다.
  • end_date는 주어진 연도의 마지막 순간(12월 31일 23:59:59)을 나타냅니다.
  • make_aware는 날짜를 timezone-aware datetime 객체로 변환합니다.

2. 각 월의 마지막 날짜 가져오기

last_days = (
    queryset
    .annotate(month=ExtractMonth('invested_at'))
    .values('month')
    .annotate(last_day=Max('invested_at'))
)

  • annotate(month=ExtractMonth('invested_at'))invested_at 필드에서 월을 추출하여 month라는 가상의 필드를 만듭니다.
  • month 로 Group_by(화)합니다.
  • annotate(last_day=Max('invested_at'))는 각 월별로 invested_at 필드의 최대값을 last_day로 계산합니다.

3. 마지막 날짜의 데이터 필터링

last_day_dates = [entry['last_day'] for entry in last_days]
return queryset.filter(invested_at__in=last_day_dates)
  • last_days 쿼리셋에서 각 월의 마지막 날짜를 리스트로 추출합니다.
  • 원래의 queryset에서 invested_atlast_day_dates에 포함된 데이터만 필터링하여 반환합니다.

Extract 에 대해서

extra 메서드

last_days = (
    queryset
    .extra(select={'month': "EXTRACT(month FROM invested_at)"})
    .values('month')
    .annotate(last_day=Max('invested_at'))
)

  • extra(select={'month': "EXTRACT(month FROM invested_at)"})는 SQL의 EXTRACT 함수를 사용하여 invested_at 필드에서 월을 추출합니다.
  • 위에서 작성한 것과 동일하게 동작합니다.

lookup

문서를 보면 코드의 스타일을 보고 프레임워크가 지향 하는 코드스타일을 파악할 수 있는데 Django 는 lookup 방식으로 많은(?) 편리함과 장점을 줍니다.

저는 실제 코드에서 look_up 방식 invested_at__month 으로 사용했습니다.

last_days = (
    queryset.values("invested_at__month")
            .annotate(last_day=Max("invested_at"))
)

Django ORM에서 ExtractMonthinvested_at__month를 사용할 때, 내부적으로 이 EXTRACT 함수를 사용하여 SQL 쿼리를 생성합니다.

(데이터베이스 시스템에 따라 EXTRACT 함수의 사용법이나 지원되는 필드가 약간씩 다를 수 있습니다. PostgreSQL, MySQL, Oracle 에서는 사용가능 합니다.)

환경

이로써 연도별 필터링과 각 월의 마지막 날짜 데이터를 필터링하는 방법에 대해 전체적으로 정리해보았습니다. 직접 SQL을 작성하는 경우 SQL 인젝션 공격에 대비한 보안을 염두해야 하겠습니다.

  • postgresql
  • Django
  • DRF

문서

extra

  • Django extra 메서드 공식 문서: extra 메서드를 사용하는 방법과 주의사항에 대해 설명합니다.

ExtractMonth

hongreat 블로그의 글을 봐주셔서 감사합니다!